import java.util.*;
import java.net.*;

public class EntryManager {
	private class SourceManager{
		Queue<Request> _requests = null;			// queue of sub requests
		Request _currentRequest = null;				// the current request that is being handle
		Request _nextRequest = null;				// waiting request
		DB _db = null;								// program DB
		private RequestSource _source = null;		// the source from which we receive the data for the current request
		private long _lastCommunication;			// the last time that data 
		long _maxIdleTime;
		
		
		public SourceManager(DB db, Request req, RequestSource source, long maxIdleTime){
			_maxIdleTime = maxIdleTime;
			_lastCommunication = System.currentTimeMillis();
			_db = db;
			_source = source;
			_currentRequest = req;
			_nextRequest = null;
			_requests = new LinkedList<Request>();
			requestEntry();
		}
		
		public RequestSource getSource() { return _source; }
		
		/**
		 * send the current request entry via the socket
		 */
		private void requestEntry(){ 
			// validates that the socket is active
			if (_source.getSource().getSocket().isAlive())
				_source.getSource().getSocket().send(_currentRequest.getRequest()); 
		}
		
		/**
		 * add a new request if relevant
		 */
		public void addRequest(Request req){
			// update dbVersion for incremental request
			req.setDbVersion(_currentRequest.getDestinationDbVer());
			Message msg = req.getRequest();
			msg.setData(("requestentry<$dbversion=" + Utils.DateToString(_currentRequest.getDestinationDbVer()) + ">").getBytes());
			// if no next request is exist
			if (_nextRequest == null){
				// if the new request's dbVersion is updater than the current request dbVersion   
				if (req.getDestinationDbVer().compareTo(_currentRequest.getDestinationDbVer()) == 1)
					_nextRequest = req;		
			}
			else{ // if next request is already exist
				// if the new request's destination date is newer than the existing next request 
				if (req.getDestinationDbVer().compareTo(_nextRequest.getDestinationDbVer()) == 1)
					// update the next request
					_nextRequest = req;
			}
		}
		/**
		 * Report that a specific entry was arrived, Initialize more requests if needed.
		 * @param peerName
		 * @param sn
		 * @param n
		 * @return - the status of the current request
		 */
		public boolean entryWasArrived(String peerName, int sn, int n){
					
			// if this is the first entry
			if (_currentRequest.getEntriesExpected() == -1)
				_currentRequest.setEntriesExpected(n);
			
			// if the entry n value doesn't fit to the n value of the current request
			if (_currentRequest.getEntriesExpected() != n)
				return false;
			
			//if serial number is wrong (==0)
			if (sn == 0)
				return false;
			
			// update last communication time
			_lastCommunication = System.currentTimeMillis();
			
			_currentRequest.setBit(sn, peerName);
			
			// if all expected entries was arrived
			if (_currentRequest.getEntriesArrived() == _currentRequest.getEntriesExpected()){
				// if sub-entries are exists
				if (!_requests.isEmpty())
					_currentRequest = _requests.poll();
				// if new request was received during the handling with the first request
				else if (_nextRequest != null){
					_currentRequest = _nextRequest;
					_nextRequest = null;
				}
				else
					// no more requests from the current source
					return true;
				// initialize the new request
				requestEntry();
			}
			// no all of the data expected was received from the source
			return false;
		}
		
		/**
		 * Check the status of the current request after maxIdleTime.
		 * Updates the source current communication information and Creates sub-requests if needed.
		 * @return - the status of the current request.
		 */
		public int status(){
			boolean sessionWasChanged = false;
			if (System.currentTimeMillis()-_lastCommunication < _maxIdleTime)
				//still receiving
				return 0;
			
			// assume no data will arrive in the future //
			
			Source currentSession = SessionsManager.getSocket(_source.getPeerName());
			
			// check if peer is offline
			if (currentSession == null){
				// source is not active any more
				return 2;
			}
			
			
			// check if the connection with the source was changed (IP, port, socket);
			
			if (!_source.getSource().equals(currentSession)){
					// update the connection details
					_source.setSource(currentSession);
					sessionWasChanged = true;
				}
			// generate new sub-requests //
			
			// if no entry was arrived
			if (_currentRequest.getEntriesExpected() == -1)
				requestEntry();
			else{ // if part of the entries was arrived 
				generateSubRequests();
				_currentRequest = _requests.poll();
				requestEntry();
			}
			if (sessionWasChanged)
				return 1;
			return 0;
			
		}
		/**
		 * Generates sub-requests according to status of the current request
		 */
		private void generateSubRequests(){
			int i = 1;
			int limit = _currentRequest.getEntriesExpected() + 1;
			String left = null;
			String right = null;
			int indexRight;
			indexRight = 1;
			String sEmpty = "-1";
			while (i < limit) {
				String data = null;
				if (!_currentRequest.wasArrived(i).equals(sEmpty)) {
					left = _currentRequest.wasArrived(i);
					++i;
				} else {
					int j = i;
					right = null;
					
					while (j < limit && right == null) {
						if (!_currentRequest.wasArrived(j).equals(sEmpty)) {
							right = _currentRequest.wasArrived(j);
							indexRight = j;
						}
						else 
							++j;
					}
				
					if (right == null) { // now - ALL {
						//data = String.format("REQUESTENTRY<%s,%s,ALL>", Utils.DateToString(_currentRequest.getDbVersion()),left);
						data = String.format("requestentry<$dbversion=%s,$range(%s,ALL)>", Utils.DateToString(_currentRequest.getDbVersion()),left);
						i = limit; // i = j;
						// create new request
						_requests.add(new Request(_currentRequest.getDbVersion(),
								_currentRequest.getDestinationDbVer(),
								new Message(data.getBytes(), _source.getSource().getIP(),_source.getSource().getPort(), _source.getPeerName())));
					} else { //left - right
						if (left == null) {
							//SEND with 0 param
							data = String.format("requestentry<$dbversion=%s,$range(0,%s)>", Utils.DateToString(_currentRequest.getDbVersion()),right);
						} else {
							//send left - right
							data = String.format("requestentry<$dbversion=%s,$range(%s-%s)>", Utils.DateToString(_currentRequest.getDbVersion()), left, right);
						}
						// create new request
						_requests.add(new Request(_currentRequest.getDbVersion(),
								_currentRequest.getDestinationDbVer(),
								new Message(data.getBytes(),_source.getSource().getIP(),_source.getSource().getPort(), _source.getPeerName())));
						i = indexRight;
					}
				}
				
			}
		}
		
	}
	
	private DB _db = null;										// program DB
	private Map<RequestSource,SourceManager> _jobs = null;		// hash map of SourceManager objects
	private PictureManager _pictureManager = null;				// a reference to the PictureManager object
	private long _maxIdleTime;									// maximum idle time
	
	/**
	 * Constructor
	 * @param db
	 * @param maxIdleTime
	 * @param pictureManager
	 */
	public EntryManager(DB db, long maxIdleTime, PictureManager pictureManager){
		_db = db;
		_jobs = new HashMap<RequestSource, SourceManager>();
		_maxIdleTime = maxIdleTime;
		_pictureManager = pictureManager;
	}
	
	/**
	 * Request entries from new source
	 * @param dbVersion
	 * @param dbDestinationVer
	 * @param peerName
	 */
	public void requestUpdates(Date dbVersion, Date dbDestinationVer, String peerName){
		// get source details
		Source sourceCon = SessionsManager.getSocket(peerName);
		
		// if no active connection is exist
		if (sourceCon == null)
			return;
		
		// create key
		RequestSource reqSource = new RequestSource(peerName,dbVersion,sourceCon);
		
		// generate request entry message
		Message msg = requestEntry(dbVersion, sourceCon.getIP(), sourceCon.getPort(), peerName);
		
		// generate new request
		Request newReq = new Request(dbVersion, dbDestinationVer, msg);
		
		// look for open request from the same source
		SourceManager source = _jobs.get(reqSource);
		// if this is the first request from this source
		if (source == null)
			// create new source manager
			source = new SourceManager(_db, newReq, reqSource, _maxIdleTime);
		else // if this is not the first request from this source
			// update the existing SourceManager with the new request
			source.addRequest(newReq);
		// insert / update the hashmap with the current SourceManager
		_jobs.put(reqSource, source);
		
			
	}
	
	/**
	 * Create new basic "requestentry" message
	 * @param dbVersion
	 * @param sourceIP
	 * @param sourcePort
	 * @param peerName
	 * @return - Message object
	 */
	private Message requestEntry(Date dbVersion, InetAddress sourceIP, int sourcePort, String peerName){
		return new Message(("requestentry<$dbversion=" + Utils.DateToString(dbVersion) + ">").getBytes(), sourceIP, sourcePort, peerName);
	}
	
	/**
	 * Handles incoming messages ("entry" messages).
	 * @param msg
	 */
	public void handle(Message msg){
		RequestSource reqSource = new RequestSource(new Source(msg.getAddress(),msg.getPort()));
		
		SourceManager source = _jobs.get(reqSource);

		// if no entries are expected from this source
		if (source == null)
			return;
		// get the current Request that is being handled
	
		String entry = new String(msg.getData());
		String[] fields = Utils.parseMessage(entry);
		int n = Integer.parseInt(fields[1]);
		int sn = Integer.parseInt(fields[0]);
		Record newEntry = new Record(fields[2],fields[3],fields[4],fields[5],fields[6],fields[7],fields[8],fields[9],fields[10],fields[11],fields[12],fields[13]);
				
		boolean result = source.entryWasArrived(newEntry.get_peerName(), sn, n);
		
		// if no more entries are expected
		if (result)
			_jobs.remove(reqSource);
		
		// Update DB with the new entry
		
		
		// get current peer record existing inside the DB
		Record currentPeer = _db.getRecord(newEntry.get_peerName());
		
		// if owner record was received from source
		if (newEntry.get_peerName().equals(_db.get_OwnerName()))
			// ignore it
			return;
		
		// if the new peer's entry includes new picture version than the current peer's entry 
		if (currentPeer == null || currentPeer.get_peerVersion().equals(Utils.zeroDate()) 
				|| newEntry.get_picVersion().compareTo(currentPeer.get_picVersion()) == 1)
				// request the new picture
				_pictureManager.requestPicture(newEntry, source.getSource());
		else // if no picture is required
			// update DB
			_db.addRecord(newEntry.get_peerName(), newEntry);
		
			
	}
	
	/**
	 * Check the status of all active requests from all sources
	 */
	public void status(){
		Set<RequestSource> sources = _jobs.keySet();
		for(RequestSource s : sources){
			int state = _jobs.get(s).status();
			// if source is not active any more
			if (state == 2)
				// remove its SourceManager from HashMap
				_jobs.remove(s);
			// if peer's session details were changed
			else if (state == 1){
				SourceManager manager = _jobs.get(s);
				// get updated information
				Source source = SessionsManager.getSocket(s.getPeerName());
				if (source == null){
					_jobs.remove(s);
					continue;
				}
				// create new key
				RequestSource key = new RequestSource(source);
				// insert request information with the new key to the hash map
				_jobs.put(key, manager);
				
				// if the new key is different from the old one
				if (!key.equals(s))
				// remove the old key
					_jobs.remove(s);
			}
			
		}
		
	}
	
}
